#==============================================================================
#
#  $Id: pos.py,v 1.4 2001/06/12 00:54:35 mike Exp $
#
"""
   Contains a set of classes that implement a symple persistent object
   service for DOPY.

   This system persists hierarchies of python objects as directory trees
   containing pickled representations of the objects.
"""
#
#  Copyright (C) 1999 Michael A. Muller
#
#  Permission is granted to use, modify and redistribute this code,
#  providing that the following conditions are met:
#
#  1) This copyright/licensing notice must remain intact.
#  2) If the code is modified and redistributed, the modifications must 
#  include documentation indicating that the code has been modified.
#  3) The author(s) of this code must be indemnified and held harmless
#  against any damage resulting from the use of this code.
#
#  This code comes with ABSOLUTELY NO WARRANTEE, not even the implied 
#  warrantee of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
#
#  $Log: pos.py,v $
#  Revision 1.4  2001/06/12 00:54:35  mike
#  Documentation fixes.
#
#  Revision 1.3  1999/09/13 18:54:34  mike
#  Fixed pickling to use cPickle
#
#  Revision 1.2  1999/07/09 14:26:31  mike
#  Forgot to take out debug prints.
#
#  Revision 1.1  1999/07/09 13:42:51  mike
#  First cut at persistence services.
#
#
#==============================================================================

import os
try:
   from cPickle import Pickler, Unpickler
except ImportError:
   from pickle import Pickler, Unpickler
from UserDict import UserDict
from naming import NamingContext
from dopy import DynamicRemoteObjectFactory, getHub

_activeObjects = {}
if getHub().isThreaded():
   import thread
   _activeObjectLock = thread.allocate_lock()
else:
   _activeObjectLock = None

class _InUse:
   """
      Private class for tracking objects that are currently in use.
      
      Public variables:
      /obj/::
         The object in question
      /useCnt/::
         A counter for the number of places that this object is being used.
   """
   def __init__(self, obj):
      self.obj = obj
      self.useCnt = 0

class ObjectInUseError(Exception):
   """
      Raised when an attempt is made to change or delete a persistent object
      that is currently in use.
   """
   pass

class FileSysDirectory(NamingContext):
   
   """
      Wrapper for a file system directory: a persistance repository based
      on the file system.
      
      This class allows you to treat a file system directory like a 
      dictionary.  All files in the directory are assumed to be pickled
      python objects.
      
      Public Variables:
      /path/::
         The fully qualified path to the directory.
      /fullKey/::
         The full object key (basically the relative path to the object
         encoded as a unix filename).  A string.
   """
   
   def __init__(self, path, fullKey):
      self.path = path
      self.fullKey = fullKey

   def getRemote(self, key, factory = DynamicRemoteObjectFactory()):
      """
         Returns a remote object that can be used to reference an object in
         the storage system.  If /key/ does not correspond to a file in the
         directory, raises a *KeyError*.
         
         A /factory/ instance may be provided to create something other than
         a DynamicRemoteObject.
      """
      localObject = self[key]
      return factory.create(None, 
                            self.__makeKey(key),
                            localObject
                            )
   
   def __getitem__(self, key):
      """
         Gets an item from the directory.  If there is a sub-directory named
         /key/, a *FileSysDirectory* object will be returned for it.  If there
         is a file named /key/ in the directory, an attempt will be made
         to load a single pickled instance from the file.
         
         When called remotely, this returns a _copy_ of the object.  Clients
         should use the @getRemote() method to obtain a remote object.
      """
      newpath = os.path.join(self.path, key)
      if _activeObjectLock: _activeObjectLock.acquire()
      try:
         # check to see if it is already in the active objects cache
         if _activeObjects.has_key(newpath):
            tmp = _activeObjects[newpath]
            return tmp.obj
            
         if os.path.isdir(newpath):
            return FileSysDirectory(newpath, self.makeKey(key))
         elif os.path.exists(newpath):
            obj = Unpickler(open(newpath)).load()
            _activeObjects[newpath] = _InUse(obj)
            if hasattr(obj, 'dopy_setPath'):
               obj.dopy_setPath(newpath)
            return obj
         else:
            raise KeyError(key)
      finally:
         if _activeObjectLock: _activeObjectLock.release()
   
   def __setitem__(self, key, val):
      """
         If /val/ is a *FileSysDirectory* or *None*, tries to create a
	 directory for it.  If it is any other kind of object, tries
         to pickle it into a file named after the key.
         
         /key/ must be a string.
         
         This method will raise an @ObjectInUseError if the object can not
         be overwritten because it is currently in use.
      """
      newpath = os.path.join(self.path, key)
      if _activeObjectLock: _activeObjectLock.acquire()
      try:
         # if the object is in use, we have to decline
         if _activeObjects.has_key(newpath):
            raise ObjectInUseError(self.__makeKey(key))
         
         if val is None or isinstance(val, FileSysDirectory):
            os.mkdir(newpath)
         else:
            Pickler(open(newpath, 'wb')).dump(val)
      finally:
         if _activeObjectLock: _activeObjectLock.release()
         
   def __delitem__(self, key):
      """
         Attempts to delete the file or sub-directory indicated by /key/,
         which must be a string.
      
         Raises an @ObjectInUseError if the object is currently being used
         in a remote method invocation.   Raises a *KeyError* if there
         is no such file.
         
         Subdirectory removal is not implemented yet.
      """
      if _activeObjectLock: _activeObjectLock.acquire()
      try:
         newpath = os.path.join(self.path, key)
         
         # if the object is in use, we have to decline
         if _activeObjects.has_key(newpath):
            raise ObjectInUseError(self.__makeKey(key))

         if os.path.isdir(newpath):
            raise NotImplementedError('directory deletion')
         
         if not os.path.exists(newpath):
            raise KeyError(key)
         
         os.remove(newpath)

      finally:
         if _activeObjectLock: _activeObjectLock.release()
         
   def keys(self):
      return os.listdir(self.path)
   
   def items(self):
      keys = self.keys()
      items = []
      for k in keys:
         items.append(k, self[k])
      return items
   
   def values(self):
      keys = self.keys()
      values = []
      for k in keys:
         values.append(self[k])
      return values

   def has_key(self, key):
      newpath = os.path.join(self.path, key)
      return os.path.exists(key)

   def __makeKey(self, key):
      """
         Private method to create a full key from the partial key of a 
         child.
      """
      return self.fullKey and self.fullKey + '/' + key or key

class PersistentObject:
   
   """
      Objects wishing to be persistent should usually derive from this
      class: it implements the `dopy_afterMethod()` method to write the
      object after every method invocation.
      
      Alternatively, users may wish to implement this functionality
      themselves so as to be more selective about when they write the object.
      If you choose to implement this yourself, you may also wish to implement
      the `_dopy_setpath()` method to allow the pathname of the object to
      be communicated upon activation.
   """
   
   def __init__(self):
      self.__path = None

   def _acquire(self):
      """
         Manages the cache.  Derived classes should call this if they override
         `dopy_beforeMethod()`.
      """
      if _activeObjectLock: _activeObjectLock.acquire()
      try:
         tmp = _activeObjects[self.__path]
         tmp.useCnt = tmp.useCnt + 1
      finally:
         if _activeObjectLock: _activeObjectLock.release()         

   def _release(self):
      """
         Manages the cache.  Derived classes should call this if they override
         `dopy_afterMethod()`.
      """
      if _activeObjectLock: _activeObjectLock.acquire()
      try:
         tmp = _activeObjects[self.__path]
         tmp.useCnt = tmp.useCnt - 1
         if not tmp.useCnt:
            del _activeObjects[self.__path]
      finally:
         if _activeObjectLock: _activeObjectLock.release()
   
   def dopy_setPath(self, pathname):
      """
         Called by @FileSysDirectory upon loading the object.  Sets the
         path so that the object can be saved to the correct file
         by `dopy_afterMethod()`.
      """
      self.__path = pathname

   def dopy_beforeMethod(self, request):
      """
         Called by @dopy.Hub.__processRequest() before a remote method
         invocation.  Increments the use count associated with /self/ in the
         cache.
      """
      self._acquire()
   
   def dopy_afterMethod(self, request, response):
      """
         Called by @dopy.Hub.__processRequest() after a remote method
         invocation.  Writes /self/ to its file.
      """
      Pickler(open(self.__path, 'wb')).dump(self)
      self._release()

class POSTest(PersistentObject):

   """
      Test of all this good stuff.
   """
   
   def __init__(self):
      PersistentObject.__init__(self)      
      self.val = 0
   
   def add(self, otherVal):
      self.val = self.val + otherVal
   
   def get(self):
      return self.val

   def slowTest(self):
      print 'in slow test'
      import time
      time.sleep(2)
